package core;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Observable;

import javax.swing.JOptionPane;
import javax.swing.Timer;

import projectiles.Nuke;
import projectiles.Projectile;
import towers.Tower;
import enemies.Enemy;
import enemies.Infantry;
import enemies.Jeep;
import enemies.Tank;

public class Model extends Observable {
	
	private ArrayList<Projectile> projectiles = new ArrayList<Projectile>();
	private ArrayList<Enemy> enemies = new ArrayList<Enemy>();
	private ArrayList<Tower> towers = new ArrayList<Tower>();
	private ArrayList<Explosion> explosions = new ArrayList<Explosion>();
	
	private Map map;
	private Statistics stats;
	private Timer timer;
	
	private String firstWave = "Infantry";
	private String secondWave = "Jeep";
	
	// Nodig om te kijken of er projectielen etc buiten het scherm vallen
	public static final int battlefieldWidth = 780;
	public static final int battlefieldHeight = 425;

	public final static double timerInterval = 0.02; // 50x per sec

	public Model( Map map ) {
		timer = new Timer( (int)(1000 * timerInterval), new TimerHandler() );
		timer.start();
		
		this.stats = new Statistics( this );
		this.map = map;
	}
	
	/**
	 * @param level
	 * @return aantal enemies dat er komen voor dat level
	 */
	public int getCountEnemies( int level ) {
		return (int)(level / 3.0 + 1);
	}
	
	/**
	 * Volgende wave van enemies
	 */
	public void nextWave(){
		if ( map.getBase().getHealth() > 0 && !isPaused() )
		{
			stats.updateNextWave();
			
			// Level verhogen
			this.stats.setLevel( this.stats.getLevel() + 1 );
			
			// Eerste punt bepalen
			int random = (int)(Math.random() * map.getRoutes().size() );
			ArrayList<Point2D.Double> randomRoute = map.getRoutes().get( random );
			
			Point2D.Double pos = randomRoute.get( 0 );
			int x = 0;
			
			// Nieuwe enemies toevoegen aan arraylist
			for( int i = 0; i < getCountEnemies(stats.getLevel()); i++ ){
				//HARDCODED: health, speed
				// Tanks en jeep zijn natuurlijk iets sterker dan infantry
				x -= 35;
				if(firstWave.equals("Tank")){
					x -= 15;
					enemies.add( new Tank( randomRoute, new Point2D.Double(pos.x + x, pos.y), stats.getLevel() * 17, (int)Math.sqrt(stats.getLevel()) * 15 + 50 ) );
				}if(firstWave.equals("Jeep")){
					x -= 5;
					enemies.add( new Jeep( randomRoute, new Point2D.Double(pos.x + x, pos.y), stats.getLevel() * 16, (int)Math.sqrt(stats.getLevel()) * 15 + 50 ) );
				}if(firstWave.equals("Infantry")){
					enemies.add( new Infantry( randomRoute, new Point2D.Double(pos.x + x, pos.y), stats.getLevel() * 15, (int)Math.sqrt(stats.getLevel()) * 15 + 50 ) );
				}
			}
			
			//first en second bepalen
			int rand1 = (int) (Math.random() * 3);
			firstWave = secondWave;
			if(rand1 == 0){
				secondWave = "Tank";
			}if(rand1 == 1){
				secondWave = "Jeep";
			}if(rand1 == 2){
				secondWave = "Infantry";
			}
			
			setChanged();
			notifyObservers( "nextwave" );
			
			setChanged();
			notifyObservers( "update" );
		}
	}
	
	public boolean buyTower( Tower tower, Point2D.Double p ){
		if( map.getBase().getHealth() > 0 && stats.getMoney() >= tower.getCost()  ){

			stats.setMoney( stats.getMoney() - tower.getCost() );
			tower.setPosition( p );
			tower.calcBounds();
			towers.add( tower );
			
			// Laat View weten dat we moeten repainten
			setChanged();
			notifyObservers( "repaint" );
			
			return true;		
		}
		return false;
	}

	/**
	 * @return the map
	 */
	public Map getMap() {
		return map;
	}

	/**
	 * @return the enemies
	 */
	public ArrayList<Enemy> getEnemies() {
		return enemies;
	}

	/**
	 * @param enemies
	 *            the enemies to set
	 */
	public void setEnemies(ArrayList<Enemy> enemies) {
		this.enemies = enemies;
	}

	/**
	 * @return the projectiles
	 */
	public ArrayList<Projectile> getProjectiles() {
		return projectiles;
	}

	/**
	 * @param projectiles
	 *            the projectiles to set
	 */
	public void setProjectiles(ArrayList<Projectile> projectiles) {
		this.projectiles = projectiles;
	}

	/**
	 * @return the towers
	 */
	public ArrayList<Tower> getTowers() {
		return towers;
	}

	/**
	 * @return the explosions
	 */
	public ArrayList<Explosion> getExplosions() {
		return explosions;
	}

	/**
	 * @param towers
	 *            the towers to set
	 */
	public void setTowers(ArrayList<Tower> towers) {
		this.towers = towers;
	}
	
	public Statistics getStats() {
		return stats;
	}
	
	public void togglePause()
	{
		if ( timer.isRunning() )
		{
			timer.stop();
			
			stats.paused();
		}
		else
		{
			timer.start();
			
			stats.unpaused();
		}
		
		setChanged();
		notifyObservers( "repaint" );
	}
	
	public boolean isPaused()
	{
		return !timer.isRunning();
	}
	
	public boolean repair(Tower t) {	
		if ( t != null && t.getHealth() > 0 && t.getHealth() < t.getMaxHealth() && t.getPosition() != null) {
			//De kosten zijn de originele kosten maal health percentage en 10% minder omdat het goedkoper moet zijn dan nieuw bouwen
			int cost = (int)( t.getCost() * (1-t.getHealthPercentage()) * 0.9);
			
			if ( stats.getMoney() >= cost)
			{
				t.setHealth( t.getMaxHealth() );
				stats.reduceMoney(cost);
				
				System.out.println("Toren gerepareerd");
				
				// Repaint de health die erbij staat
				setChanged();
				notifyObservers( "repaint" );
				return true;
			}
		}
		return false;
	}
	
	public boolean sell(Tower t) {
		if ( t != null && t.getHealth() > 0 && t.getPosition() != null )
		{
			towers.remove(t);
			
			//De opbrengsten zijn 75% van de originele kosten, maal health percentage
			int profit = (int) (0.75 * t.getCost() * t.getHealthPercentage());
			stats.increaseMoney(profit);
			
			t.setHealth(0);
			
			//Tower doet boom!
			explosions.add( new Explosion( t.getMiddle(), 50) );
			
			System.out.println("Toren verkocht");
			
			setChanged();
			notifyObservers( "repaint" );
			
			return true;
		}
		return false;
	}
	
	public void repaint(){
		setChanged();
		notifyObservers( "repaint" );
	}
	
	class TimerHandler implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			
			// Moeten we repainten?
			boolean repaint = false;
			
			// Spel is afgelopen als basis kapot is
			// We hoeven niets meer up te daten of te redrawen, alles behalve de explosies
			
			// Als health van basis 0 is, dan doe explosie
			if( map.getBase().getHealth() == 0) {
				System.out.println("Basis is kapot");
				
				explosions.add( new Explosion( map.getBase().getMiddle(), 175, true) );
				repaint = true;
				map.getBase().setHealth(-1);
				
				showInputName();
				stats.insertStats();
				showHighScore();
			}
			else if (map.getBase().getHealth() > 0 ) {
				
				if ( !isPaused() )
				{
					// Stats updaten als spel niet gepauzeerd is
					stats.update();
				
					setChanged();
					notifyObservers( "stats" );
					
					// next wave
					if ( stats.getNextWave() <= 0 )
						nextWave();
				}
			
				// Bijhouden welke units weg mogen
				ArrayList<Enemy> deadEnemies = new ArrayList<Enemy>();
				ArrayList<Projectile> deadProjectiles = new ArrayList<Projectile>();
				ArrayList<Tower> deadTowers = new ArrayList<Tower>();
				
				for( Tower t : towers ){
					if ( t.update() )
						repaint = true;
				}
				
				for( Projectile p : projectiles ){
					if ( p.update() )
						repaint = true;
					
					if ( p.isOutsideView() )
						deadProjectiles.add( p );
				}
				
				for( Enemy en : enemies ){
					if ( en.update() )
						repaint = true;
				}
	
				
				// Torens in de basis verwijderen
				for( Enemy en : enemies) {
					if(en.getHealth() > 0 && en.bounds.intersects(map.getBase().position.x, map.getBase().position.y, map.getBase().getSize().getWidth(), map.getBase().getSize().getHeight())) {
						
						// De enemy kan niets meer doen
						en.setHealth(0);
						
						//Enemy verwijderen
						deadEnemies.add(en);
						
						//Base health verminderen
						map.getBase().setHealth( Math.max( map.getBase().getHealth() - 1, 0 ) );
					}
				}
				
				// Torens laten schieten
				for( Tower t : towers ){
					for( Enemy en : enemies ){
						if ( !en.isOutsideView() && en.getHealth() > 0 && en.position.distance( t.position ) <= t.range )
						{
							t.setAimAtEnemy( en );
							
							if ( t.canShoot( ) )
							{
								projectiles.add( t.shoot() );
								repaint = true;
							}
						}
					}
				}
				
				// Vijanden laten schieten
				for( Enemy en : enemies ){
					for( Tower t : towers ){
						if ( en.position.distance( t.position ) <= en.range )
						{
							en.setTower( t );
							
							if ( en.canShoot( ) )
							{
								projectiles.add( en.shoot() );
								repaint = true;
							}
						}
					}
				}
				
				// Projectile kan wat raken
				ArrayList<ShootingUnit> shootingUnits = new ArrayList<ShootingUnit>();
				shootingUnits.addAll( towers );
				shootingUnits.addAll( enemies );
				
				for( Projectile t : projectiles ){
					// Nuke controleren
					if ( t instanceof Nuke ){
						Nuke n = (Nuke) t;
						
						// Nen nieuwe enemy toewijzen als nuke geen (levende) vijand heeft
						if( n == null || n.getEnemy().getHealth() <= 0){
							n.calcEnemyForNuke( enemies );
						}
						
					}
					
					for( ShootingUnit en : shootingUnits ){
						if ( t.bounds.intersects( en.getPosition().x, en.getPosition().y, en.getSize().getWidth(), en.getSize().getHeight()))
						{
							// Ze mogen niet op hetzelfde type schieten
							if ( (t.getOrigin() instanceof Tower && en instanceof Enemy) || (t.getOrigin() instanceof Enemy && en instanceof Tower) )
							{
								int health = Math.max( 0, en.getHealth() - t.getDamage() );
								if ( health == 0 )
								{
									if ( en instanceof Enemy )
									{
										stats.incrementKills();
										// Je krijgt geld erbij als je een enemy afmaakt
										// de wortel van level zodat als level hoger word, je niet teveel krijgt
										// delen door 30 om het flink te verlagen en + 10 zodat je niet te weinig krijgt in de eerste levels
										stats.setMoney( (int) (stats.getMoney() + (Math.sqrt( stats.getLevel() ) / 30 + 10) ) );
										deadEnemies.add( (Enemy)en );
										
										// Nuke explodeert
										if ( t instanceof Nuke ){
											explosions.add( new Explosion( t.getMiddle(), 40) );
										}
									}
									else if ( en instanceof Tower)
									{
										deadTowers.add( (Tower)en );
										
										// Tower doet boom!
										explosions.add( new Explosion( en.getMiddle(), 50) );
									}
								}
								
								en.setHealth( health );
								
								// Projectile ook verwijderen
								deadProjectiles.add( t );
							}
						}
					}
				}
				
				// Verwijder dead units
				for( Enemy en : deadEnemies ){
					repaint = true;
					enemies.remove( en );
				}
				deadEnemies.clear();
				for( Projectile p : deadProjectiles ){
					repaint = true;
					projectiles.remove( p );
				}
				deadProjectiles.clear();
				for( Tower t : deadTowers ){
					repaint = true;
					towers.remove( t );
				}
				deadTowers.clear();
				
				// Update stats
				setChanged();
				notifyObservers( "updateStats" );
			}
			
			// Update explosions
			// Verwijder explosions die klaar zijn
			ArrayList<Explosion> doneExplosions = new ArrayList<Explosion>();
			for( Explosion ex : explosions ) {
				
				if ( ex.isDone() )
					doneExplosions.add( ex );
			}
			
			for( Explosion ex : doneExplosions ) {
				explosions.remove( ex );
			}
			doneExplosions.clear();
			
			// Laat view weten dat we graag willen repainten
			if ( repaint ) {
				setChanged();
				notifyObservers( "repaint" );
			}
			
			//Elke timer tik de stats laten updaten
			stats.update();
		}
	}


	/**
	 * @return the firstWave
	 */
	public String getFirstWave() {
		return firstWave;
	}

	/**
	 * @return the secondWave
	 */
	public String getSecondWave() {
		return secondWave;
	}
	
	public void showInputName(){
		String name = JOptionPane.showInputDialog("What is your name?");
		String email = JOptionPane.showInputDialog("What is your email?");
		getStats().setName(name);
		getStats().setEmail(email);
	}
	public void showHighScore(){
		setChanged();
		notifyObservers( "highscore" );
	
	}
}
